#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @Time    : 2025/6/17 21:37
# @Author  : wuyupeng
# @FileName: apt_repo_manager.py
# @Software: PyCharm
import os
import abc
import gzip
import shutil
import logging
import hashlib
import tempfile
import subprocess
from pathlib import Path
from debian.deb822 import Deb822
from datetime import datetime, timezone

from repo_uploaders.repo_manager import RepoManager
from gpg_key_manager import GPGKeyManager


logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s: %(message)s")


class PipeLineLogHelper(abc.ABC):
    def upload_foler(self, local_path: Path, remote_key: str):
        """ 上传整个文件路径 """

    @abc.abstractmethod
    def get_file(self, remote_key, download_path):
        """ 把 remote_key 下载到 download_path """

    @abc.abstractmethod
    def upload_file(self, remote_key: str, local_file: str):
        """ 把 local_file 上传为 remote_key """


class AptRepoManager(RepoManager):
    
    DEB_PACKAGE = "Package"
    DEB_VERSION = "Version"

    def __init__(self, pkg_path, codename, component, arch, remote_root, is_debug=False):
        super().__init__(pkg_path, is_debug)
        self._remote_root = Path(remote_root)
        self._pkg_codename = codename
        self._pkg_component = component
        self._pkg_arch = arch
        self._remote_pkgs = ''
        self.apt_helper = None
        self.gpg_mgr = GPGKeyManager()
        self._is_first_pkg = False
        self.remove_file = None

    @abc.abstractmethod
    def init_apt_helper(self, *args, **kwargs):
        """ 初始化上传组件 self.apt_helper """
    
    def construct_package_locally(self, private_key, passphrase, *args, **kwargs):
        target_path = Path(".").resolve() / "target"
        is_upload = self._pkg_path.is_file() and self._pkg_path.suffix == ".deb"
        with tempfile.TemporaryDirectory() as tmp_dir:
            root_path = Path(tmp_dir)
            
            release_path = root_path / self.release_path
            inrelease_path = root_path / self.inrelease_path
            release_gpg_path = root_path / self.release_gpg_path
            
            pkg_path = root_path / self.pakcages_path
            pkg_gz = root_path / self.pakcages_gz_path
            pkg_name = f"{self._pkg_name}_{self._pkg_version}_{self._pkg_arch}.deb"
            dest_pkg_path = root_path / self.pool_path / pkg_name.replace("-", "_")

            for f in [release_path, inrelease_path, release_gpg_path, pkg_path, pkg_gz]:
                f.parent.mkdir(exist_ok=True, parents=True)
            if is_upload: # 创建文件
                dest_pkg_path.parent.mkdir(exist_ok=True, parents=True)

            if not self.apt_helper:
                logging.info(f"桶管理器未初始化，进行本地构建")
            else:
                logging.info(f"获取包列表")
                remote_pkg_path = root_path / self.pakcages_gz_path.name
                pkgs_resp = self.apt_helper.get_file(self.pakcages_gz_path, str(remote_pkg_path))
                if pkgs_resp.status == 404:
                    logging.info("仓库为空，创建第一个包")
                    self._is_first_pkg = True
                elif pkgs_resp.status >= 300 and "NoSuchKey" not in str(pkgs_resp.error):
                    raise ValueError("仓库没有 Pakcages 文件")
                
                if remote_pkg_path.exists():
                    with gzip.open(remote_pkg_path, "rt") as f:
                        self._remote_pkgs = f.read()

            # 1 生成 Packages & Packages.gz
            if is_upload: # 添加新包的package信息
                self._generate_package_file(root_path, dest_pkg_path, pkg_path, pkg_gz)
            else: # 删除指定包的package信息
                self._update_packages_when_delete_file(pkg_path, pkg_gz)
                self.remove_file = dest_pkg_path.relative_to(root_path)
            # 2. 生成 Release
            self._generate_release_file(release_path, pkg_path, pkg_gz)
            # 3. 签名 Release
            self._sign_release_file(private_key, passphrase, release_path, inrelease_path, release_gpg_path)

            if target_path.exists():
                shutil.rmtree(target_path)
            shutil.copytree(root_path, target_path, copy_function=shutil.copy2)

        return target_path

    def parse_name_and_version(self):
        if not self._pkg_path.exists() or not self._pkg_path.is_file() or self._pkg_path.suffix != ".deb":
            raise FileNotFoundError(f"APT 文件不存在：{self._pkg_path}")

        cur_pkg = subprocess.run(["dpkg", "-I", str(self._pkg_path), "control"], capture_output=True, text=True, check=True)
        control_info = Deb822(cur_pkg.stdout)
        self.debug(control_info)
        self._pkg_name = control_info[self.DEB_PACKAGE]
        self._pkg_version = control_info[self.DEB_VERSION]
    
    def check_version_exist(self):
        if self._is_first_pkg:
            return False

        if not self._remote_pkgs:
            raise ValueError("下载 Packages 失败!")
        
        curr_pkgs = []
        for pkg in Deb822.iter_paragraphs(self._remote_pkgs):
            if pkg.get(self.DEB_PACKAGE) == self._pkg_name:
                curr_pkgs.append(pkg.get(self.DEB_VERSION))
        
        logging.info(f"当前 apt 包版本: {self._pkg_version}")
        logging.info(f"可获取版本：{curr_pkgs}")
        return self._pkg_version in curr_pkgs

    def push_to_repo(self, pkg_root: Path, remote_public_key, *args, **kwargs):
        if not self.apt_helper:
            raise ValueError(f"请先调用 init_apt_helper 进行桶链接初始化！")
        
        if not pkg_root.exists():
            raise ValueError(f"包路径不存在: {pkg_root}")

        with tempfile.TemporaryDirectory() as tmpdir:
            tmp_pk = Path(tmpdir) / "public.key"
            self.apt_helper.get_file(remote_public_key, str(tmp_pk))

            if not tmp_pk.is_file():
                raise RuntimeError("下载公钥失败!")

            mgr = GPGKeyManager(tmpdir)
            with open(tmp_pk, 'rb') as pkf:
                mgr.import_key(pkf.read())

            valid = mgr.verify_detached(str(pkg_root / self.release_path), str(pkg_root / self.release_gpg_path))
            if not valid.valid:
                raise RuntimeError(f"公钥验证文件失败: {valid.status}")

        # 上传所有文件
        self.apt_helper.upload_foler(pkg_root)

        logging.info("上传 APT 成功！")

    def delete_package(self, *args, **kwargs) -> bool:
        if not self.apt_helper:
            raise ValueError(f"请先调用 init_apt_helper 进行桶链接初始化！")
        if self.remove_file is None:
            raise ValueError(f"待删文件路径未初始化！")
        
        logging.info(f"开始删除 {self.remove_file}")
        # 删除远端Deb包
        self.apt_helper.delete_file(self.remove_file)
    
    def get_file(self, remote_key, download_path):
        if not self.apt_helper:
            raise ValueError(f"请先调用 init_apt_helper 进行桶链接初始化！")
        self.apt_helper.get_file(remote_key, download_path)

    def _update_packages_when_delete_file(self, packages_path, packages_gz_path):
        # 过滤待删文件后更新_remote_pkgs
        packages = []
        for pkg in Deb822.iter_paragraphs(self._remote_pkgs):
            packages.append(pkg)
        
        filtered_packages = [
            pkg for pkg in packages
            if not (pkg.get(self.DEB_PACKAGE) == self._pkg_name and pkg.get(self.DEB_VERSION) == self._pkg_version)
        ]
        self._remote_pkgs = "\n".join(str(pkg) for pkg in filtered_packages) + "\n"
        
        with packages_path.open("w") as f:
            f.write(self._remote_pkgs)
            logging.info(f"生成 {packages_path}")

        with packages_path.open('rb') as f_in, gzip.open(packages_gz_path, 'wb') as f_out:
            f_out.writelines(f_in)
            logging.info(f"生成 {packages_gz_path}")

    def _generate_package_file(self, root_dir, dest_pkg_path, packages_path, packages_gz_path):
        dest_pkg_path.write_bytes(self._pkg_path.read_bytes())
        self.debug(f"dest_pkg_path {dest_pkg_path}")
        self.debug(f"self._pkg_path {self._pkg_path}")
        self.debug(f"self.pool_path.relative_to(self._remote_root) {self.pool_path.relative_to(self._remote_root)}")
        self.debug(f"root_dir / self._remote_root {root_dir / self._remote_root}")
        # 扫描本地路径，构建packages文件
        cur_pkg = subprocess.run(
            ["dpkg-scanpackages", "--arch", self._pkg_arch, str(self.pool_path.relative_to(self._remote_root)), "/dev/null"], 
            cwd=root_dir / self._remote_root, capture_output=True, text=True, check=True
        )
        self.debug(f"cur_pkg.stdout {cur_pkg.stdout}")
        if not cur_pkg.stdout:
            raise RuntimeError("生成 Pakcages 失败！")
        
        with packages_path.open("w") as f:
            f.write(self._remote_pkgs + cur_pkg.stdout)
            logging.info(f"生成 {packages_path}")

        with packages_path.open('rb') as f_in, gzip.open(packages_gz_path, 'wb') as f_out:
            f_out.writelines(f_in)
            logging.info(f"生成 {packages_gz_path}")

    def _generate_release_file(self, release_path, packages_path, packages_gz_path):
        checksums = []
        for rel_path in [packages_path, packages_gz_path]:
            content = rel_path.read_bytes()

            checksums.append({
                "md5": hashlib.md5(content).hexdigest(),
                "sha256": hashlib.sha256(content).hexdigest(),
                "size": rel_path.stat().st_size,
                "path": Path(self._pkg_component) / f"binary-{self._pkg_arch}" / rel_path.name
            })

        with release_path.open("w") as f:
            f.write("Origin: openUBMC bingo\n")
            f.write("Lable: openUBMC bingo\n")
            f.write(f"Suite: {self._pkg_codename}\n")
            f.write(f"Codename: {self._pkg_codename}\n")
            f.write(f"Architectures: {self._pkg_arch}\n")
            f.write(f"Components: {self._pkg_component}\n")
            f.write(f"Version: {self._pkg_version}\n")
            f.write("Description: 构建工具\n")
            f.write(f"Date: {datetime.now(timezone.utc).strftime('%a, %d %b %Y %H:%M:%S %Z')}\n")

            f.write("MD5Sum:\n")
            for cs in checksums:
                f.write(f" {cs['md5']} {cs['size']} {str(cs['path'])}\n")

            f.write("SHA256:\n")
            for cs in checksums:
                f.write(f" {cs['sha256']} {cs['size']} {str(cs['path'])}\n")

        logging.info(f"生成 {release_path}")

    def _sign_release_file(self, private_key, passphrase, release_path, inrelease_path, release_gpg_path):
        key_fingerprint = self.gpg_mgr.import_key(private_key)
        if not key_fingerprint:
            raise RuntimeError("导入密钥失败!")

        self.gpg_mgr.sign_clearsigned(str(release_path), key_fingerprint, passphrase, inrelease_path)
        self.gpg_mgr.sign_detached(str(release_path), key_fingerprint, passphrase, release_gpg_path)

        valid_clearsigned = self.gpg_mgr.verify_clearsigned(inrelease_path)
        valid_detached = self.gpg_mgr.verify_detached(release_path, release_gpg_path)

        raise_err = False
        if not valid_clearsigned.valid:
            logging.error(f"{inrelease_path}签名失败: ({valid_clearsigned.status})")
            raise_err = True
        if not valid_detached.valid:
            logging.error(f"{release_gpg_path}签名失败: ({valid_detached.status})")
            raise_err = True
        if raise_err:
            raise RuntimeError("验签失败！")

    @property
    def dists_path(self):
        return self._remote_root / "dists" / self._pkg_codename

    @property
    def release_path(self):
        return self.dists_path / "Release"
    
    @property
    def inrelease_path(self):
        return self.dists_path / "InRelease"
    
    @property
    def release_gpg_path(self):
        return self.dists_path / "Release.gpg"

    @property
    def pakcages_path(self):
        return self.dists_path / self._pkg_component / f"binary-{self._pkg_arch}" / "Packages"
    
    @property
    def pakcages_gz_path(self):
        return self.pakcages_path.with_name("Packages.gz")
    
    @property
    def pool_path(self):
        init_char = self._pkg_name[0].lower()
        return  self._remote_root / "pool" / self._pkg_component / init_char / self._pkg_name
